﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using AForge.Imaging;
using AForge.Imaging.Filters;
using Accord.Vision.Detection;
using Accord.Vision.Detection.Cascades;
using Eksponent.CropUp.Plugins;
using System.Linq;

namespace Eksponent.CropUp.AutoDetect
{
    /// <summary>
    /// Uses the average point of energy to suggest the gravity point
    /// </summary>
    public class FaceAndEnergyDetector : IAutoDetector
    {
        public bool DetectFaces { get; set; }


        public int WorkWidth = 800;
        public bool BoxFaces = true;
        public int FaceMargin = 140;

        public FaceAndEnergyDetector(bool detectFaces = true)
        {
            DetectFaces = detectFaces;
        }


        public CropInfo SuggestCropInfo(string virtualPath, Dialect dialect)
        {
            using (var source = dialect.GetBitmap(virtualPath))
            {
                return SuggestCropInfo(source.Bitmap);
            }
        }
        public CropInfo SuggestCropInfo(Bitmap source)
        {
            var source24 = source;
            if (source.PixelFormat != PixelFormat.Format24bppRgb)
            {
                var sourcergb = new Bitmap(source.Width, source.Height, PixelFormat.Format24bppRgb);
                using (var g = Graphics.FromImage(sourcergb))
                {
                    g.DrawImageUnscaled(source, 0, 0);
                }

                source24 = sourcergb;
            }
            try
            {
                using (var temp = Grayscale.CommonAlgorithms.RMY.Apply(source24))
                {
                    var w = Math.Min(temp.Width, WorkWidth);
                    
                    using (var b = new ResizeBicubic(w, w * temp.Height / temp.Width).Apply(temp))
                    {

                        //Detect faces
                        if (DetectFaces)
                        {
                            var info = DoFaceDection(b);
                            if (info != null)
                            {
                                return info;
                            }
                        }
                        
                        

                        new GaussianBlur(2, 7).ApplyInPlace(b);
                        new NonCrashingSobelEdgeDetector().ApplyInPlace(b);

                        var ec = new EnergyCenter();
                        ec.ApplyInPlace(b);
                        return new CropInfo { Gravity = ec.Gravity };
                        
                    }
                }
            }
            finally
            {
                if (source != source24)
                {
                    source.Dispose();
                }
            }

            return new CropInfo {Gravity = new PointF(.5f, .5f)};
        }



        CropInfo DoFaceDection(Bitmap b)
        {
            var cascade = new FaceHaarCascade();
            var detector = new HaarObjectDetector(cascade, 30);

            detector.Channel = 0;
            //This mode is used to get all rectangles
            detector.SearchMode = ObjectDetectorSearchMode.Default;
            detector.ScalingMode = ObjectDetectorScalingMode.SmallerToGreater;
            detector.ScalingFactor = 1.1f;
            detector.UseParallelProcessing = true;

            var margin = FaceMargin * b.Width / WorkWidth;

            var faces = detector.ProcessFrame(b);

            //Find overlapping rectangles and group them. False positives normally only have one or two rectangles in a group
            var marked = new bool[faces.Length];
            var groups = new List<Tuple<Rectangle, List<Rectangle>>>();
            for (var i = 0; i < faces.Length; i++)
            {
                if (!marked[i])
                {
                    marked[i] = true;

                    var list = new List<Rectangle>();
                    list.Add(faces[i]);

                    var testRect = faces[i];

                    for (var j = i + 1; j < faces.Length; j++)
                    {
                        var other = faces[j];
                        if (testRect.IntersectsWith(other))
                        {
                            marked[j] = true;
                            list.Add(other);
                            testRect = Union(testRect, other);
                        }
                    }
                    groups.Add(Tuple.Create(testRect, list));
                }
            }


            //An eigth of the image size. Otherwise it's not a interesting face.
            var minSize = Math.Min(b.Width, b.Height) / 8;

            faces = groups.Where(g => g.Item2.Count >= 3 && g.Item2.Any(r => r.Width > minSize || r.Height > minSize)).ToArray().Select(g => g.Item1).ToArray();



            if (faces.Length > 0)
            {
                //Find a rectangle that spans all faces and add some margin.
                float x1 = (faces.Min(f => f.Left) - margin) / (float)b.Width,
                       x2 = (margin + faces.Max(f => f.Right)) / (float)b.Width,
                       y1 = (faces.Min(f => f.Top) - margin) / (float)b.Height,
                       y2 = (margin + faces.Max(f => f.Bottom)) / (float)b.Height;

                //Don't allow the rectangle to exceed the image
                if (x1 < 0) x1 = 0;
                if (x2 > 1) x2 = 1;
                if (y1 < 0) y1 = 0;
                if (y2 > 1) y2 = 1;

                //NOTE: To center the gravity point as "red nose" on the biggest face looks fancy but produces poor results.
                //      It's better to put it in the center
                //var biggest = faces.OrderBy(f => f.Width * f.Height).First();

                return new CropInfo
                {
                    //Set the gravity point in the middle of the biggest rectangle
                    Gravity = new PointF(x1 + (x2 - x1) / 2, y1 + (y2 - y1) / 2),
                    TopLeft = BoxFaces ? new PointF(x1, y1) : new PointF(0, 0),
                    BottomRight = BoxFaces ? new PointF(x2, y2) : new PointF(1, 1)
                };
            }

            return null;
        }

        Rectangle Union(Rectangle r1, Rectangle r2)
        {
            var x1 = Math.Min(r1.Left, r2.Left);
            var x2 = Math.Max(r1.Right, r2.Right);
            var y1 = Math.Min(r1.Top, r2.Top);
            var y2 = Math.Max(r1.Bottom, r2.Bottom);
            return new Rectangle(x1, y1, x2 - x1, y2 - y1);
        }

        class EnergyCenter : BaseInPlaceFilter
        {
            public PointF Gravity { get; set; }

            protected unsafe override void ProcessFilter(UnmanagedImage image)
            {
                var width = image.Width;
                var height = image.Height;

                var offset = image.Stride - width;
                var p = (byte*)image.ImageData.ToPointer();
                byte value;

                byte max = 0;
                for (var y = 0; y < height; y++)
                {
                    for (var x = 0; x < width; x++, p++)
                    {
                        value = *p;
                        if (value > max) max = value;
                    }
                    p += offset;
                }


                p = (byte*)image.ImageData.ToPointer();

                var threshold = (byte)Math.Min(Math.Max(0, max - 1), 200);

                //Find global center of gravity
                double accum = 0d, accumX = 0d, accumY = 0d, dvalue = 0d;
                for (var y = 0; y < height; y++)
                {
                    for (var x = 0; x < width; x++, p++)
                    {
                        value = *p;
                        if (value > threshold)
                        {
                            value -= threshold;
                            dvalue = (value * value) / (255d * 255d);
                            accum += dvalue;
                            accumX += x * dvalue;
                            accumY += y * dvalue;
                        }
                    }
                    p += offset;
                }

                var cx = accumX / accum;
                var cy = accumY / accum;
                if (double.IsNaN(cx))
                {
                    cx = width / 2d;
                    cy = height / 2d;
                }

                Gravity = new PointF((float)cx / width, (float)cy / height);
            }

            private Dictionary<PixelFormat, PixelFormat> _formatTranslations = new Dictionary<PixelFormat, PixelFormat>()
                {
                    {PixelFormat.Format8bppIndexed, PixelFormat.Format8bppIndexed}
                };

            public override Dictionary<PixelFormat, PixelFormat> FormatTranslations
            {
                get { return _formatTranslations; }
            }
        }
    }
}